/* *
 *
 *  (c) 2010-2019 Torstein Honsi
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "./Globals.js";
/**
 * Callback JavaScript function to format the data label as a string. Note that
 * if a `format` is defined, the format takes precedence and the formatter is
 * ignored.
 *
 * @callback Highcharts.DataLabelsFormatterCallbackFunction
 *
 * @param {Highcharts.DataLabelsFormatterContextObject} this
 *        Data label context to format
 *
 * @return {number|string|null|undefined}
 *         Formatted data label text
 */
/**
 * Context for the callback function to format the data label.
 *
 * @interface Highcharts.DataLabelsFormatterContextObject
 */ /**
 * Stacked series and pies only. The point's percentage of the total.
 * @name Highcharts.DataLabelsFormatterContextObject#percentage
 * @type {number|undefined}
 */ /**
 * The point object. The point name, if defined, is available through
 * `this.point.name`.
 * @name Highcharts.DataLabelsFormatterContextObject#point
 * @type {Highcharts.Point}
 */ /**
 * The series object. The series name is available through `this.series.name`.
 * @name Highcharts.DataLabelsFormatterContextObject#series
 * @type {Highcharts.Series}
 */ /**
 * Stacked series only. The total value at this point's x value.
 * @name Highcharts.DataLabelsFormatterContextObject#total
 * @type {number|undefined}
 */ /**
 * The x value.
 * @name Highcharts.DataLabelsFormatterContextObject#x
 * @type {number}
 */ /**
 * The y value.
 * @name Highcharts.DataLabelsFormatterContextObject#y
 * @type {number|null}
 */
/**
 * Options for the series data labels, appearing next to each data point.
 *
 * Since v6.2.0, multiple data labels can be applied to each single point by
 * defining them as an array of configs.
 *
 * In styled mode, the data labels can be styled with the
 * `.highcharts-data-label-box` and `.highcharts-data-label` class names.
 *
 * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-datalabels-enabled|Highcharts-Demo:}
 *      Data labels enabled
 * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-datalabels-multiple|Highcharts-Demo:}
 *      Multiple data labels on a bar series
 * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series-datalabels|Highcharts-Demo:}
 *      Style mode example
 *
 * @interface Highcharts.DataLabelsOptionsObject
 */
/**
 * Values for handling data labels that flow outside the plot area.
 *
 * @typedef {"allow"|"justify"} Highcharts.DataLabelsOverflowValue
 */
import U from "./Utilities.js";
var animObject = U.animObject,
  arrayMax = U.arrayMax,
  clamp = U.clamp,
  defined = U.defined,
  extend = U.extend,
  isArray = U.isArray,
  objectEach = U.objectEach,
  pick = U.pick,
  relativeLength = U.relativeLength,
  splat = U.splat;
import "./Series.js";
var format = H.format,
  merge = H.merge,
  noop = H.noop,
  Series = H.Series,
  seriesTypes = H.seriesTypes,
  stableSort = H.stableSort;
/* eslint-disable valid-jsdoc */
/**
 * General distribution algorithm for distributing labels of differing size
 * along a confined length in two dimensions. The algorithm takes an array of
 * objects containing a size, a target and a rank. It will place the labels as
 * close as possible to their targets, skipping the lowest ranked labels if
 * necessary.
 *
 * @private
 * @function Highcharts.distribute
 * @param {Highcharts.DataLabelsBoxArray} boxes
 * @param {number} len
 * @param {number} maxDistance
 * @return {void}
 */
H.distribute = function (boxes, len, maxDistance) {
  var i,
    overlapping = true,
    origBoxes = boxes, // Original array will be altered with added .pos
    restBoxes = [], // The outranked overshoot
    box,
    target,
    total = 0,
    reducedLen = origBoxes.reducedLen || len;
  /**
   * @private
   */
  function sortByTarget(a, b) {
    return a.target - b.target;
  }
  // If the total size exceeds the len, remove those boxes with the lowest
  // rank
  i = boxes.length;
  while (i--) {
    total += boxes[i].size;
  }
  // Sort by rank, then slice away overshoot
  if (total > reducedLen) {
    stableSort(boxes, function (a, b) {
      return (b.rank || 0) - (a.rank || 0);
    });
    i = 0;
    total = 0;
    while (total <= reducedLen) {
      total += boxes[i].size;
      i++;
    }
    restBoxes = boxes.splice(i - 1, boxes.length);
  }
  // Order by target
  stableSort(boxes, sortByTarget);
  // So far we have been mutating the original array. Now
  // create a copy with target arrays
  boxes = boxes.map(function (box) {
    return {
      size: box.size,
      targets: [box.target],
      align: pick(box.align, 0.5),
    };
  });
  while (overlapping) {
    // Initial positions: target centered in box
    i = boxes.length;
    while (i--) {
      box = boxes[i];
      // Composite box, average of targets
      target =
        (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
      box.pos = clamp(target - box.size * box.align, 0, len - box.size);
    }
    // Detect overlap and join boxes
    i = boxes.length;
    overlapping = false;
    while (i--) {
      // Overlap
      if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) {
        // Add this size to the previous box
        boxes[i - 1].size += boxes[i].size;
        boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
        boxes[i - 1].align = 0.5;
        // Overlapping right, push left
        if (boxes[i - 1].pos + boxes[i - 1].size > len) {
          boxes[i - 1].pos = len - boxes[i - 1].size;
        }
        boxes.splice(i, 1); // Remove this item
        overlapping = true;
      }
    }
  }
  // Add the rest (hidden boxes)
  origBoxes.push.apply(origBoxes, restBoxes);
  // Now the composite boxes are placed, we need to put the original boxes
  // within them
  i = 0;
  boxes.some(function (box) {
    var posInCompositeBox = 0;
    if (
      box.targets.some(function () {
        origBoxes[i].pos = box.pos + posInCompositeBox;
        // If the distance between the position and the target exceeds
        // maxDistance, abort the loop and decrease the length in increments
        // of 10% to recursively reduce the  number of visible boxes by
        // rank. Once all boxes are within the maxDistance, we're good.
        if (Math.abs(origBoxes[i].pos - origBoxes[i].target) > maxDistance) {
          // Reset the positions that are already set
          origBoxes.slice(0, i + 1).forEach(function (box) {
            delete box.pos;
          });
          // Try with a smaller length
          origBoxes.reducedLen = (origBoxes.reducedLen || len) - len * 0.1;
          // Recurse
          if (origBoxes.reducedLen > len * 0.1) {
            H.distribute(origBoxes, len, maxDistance);
          }
          // Exceeded maxDistance => abort
          return true;
        }
        posInCompositeBox += origBoxes[i].size;
        i++;
      })
    ) {
      // Exceeded maxDistance => abort
      return true;
    }
  });
  // Add the rest (hidden) boxes and sort by target
  stableSort(origBoxes, sortByTarget);
};
/**
 * Draw the data labels
 *
 * @private
 * @function Highcharts.Series#drawDataLabels
 * @return {void}
 * @fires Highcharts.Series#event:afterDrawDataLabels
 */
Series.prototype.drawDataLabels = function () {
  var series = this,
    chart = series.chart,
    seriesOptions = series.options,
    seriesDlOptions = seriesOptions.dataLabels,
    points = series.points,
    pointOptions,
    hasRendered = series.hasRendered || 0,
    dataLabelsGroup,
    seriesAnimDuration = animObject(seriesOptions.animation).duration,
    fadeInDuration = Math.min(seriesAnimDuration, 200),
    defer =
      !chart.renderer.forExport &&
      pick(seriesDlOptions.defer, fadeInDuration > 0),
    renderer = chart.renderer;
  /**
   * Handle the dataLabels.filter option.
   * @private
   */
  function applyFilter(point, options) {
    var filter = options.filter,
      op,
      prop,
      val;
    if (filter) {
      op = filter.operator;
      prop = point[filter.property];
      val = filter.value;
      if (
        (op === ">" && prop > val) ||
        (op === "<" && prop < val) ||
        (op === ">=" && prop >= val) ||
        (op === "<=" && prop <= val) ||
        (op === "==" && prop == val) || // eslint-disable-line eqeqeq
        (op === "===" && prop === val)
      ) {
        return true;
      }
      return false;
    }
    return true;
  }
  /**
   * Merge two objects that can be arrays. If one of them is an array, the
   * other is merged into each element. If both are arrays, each element is
   * merged by index. If neither are arrays, we use normal merge.
   * @private
   */
  function mergeArrays(one, two) {
    var res = [],
      i;
    if (isArray(one) && !isArray(two)) {
      res = one.map(function (el) {
        return merge(el, two);
      });
    } else if (isArray(two) && !isArray(one)) {
      res = two.map(function (el) {
        return merge(one, el);
      });
    } else if (!isArray(one) && !isArray(two)) {
      res = merge(one, two);
    } else {
      i = Math.max(one.length, two.length);
      while (i--) {
        res[i] = merge(one[i], two[i]);
      }
    }
    return res;
  }
  // Merge in plotOptions.dataLabels for series
  seriesDlOptions = mergeArrays(
    mergeArrays(
      chart.options.plotOptions &&
        chart.options.plotOptions.series &&
        chart.options.plotOptions.series.dataLabels,
      chart.options.plotOptions &&
        chart.options.plotOptions[series.type] &&
        chart.options.plotOptions[series.type].dataLabels
    ),
    seriesDlOptions
  );
  H.fireEvent(this, "drawDataLabels");
  if (
    isArray(seriesDlOptions) ||
    seriesDlOptions.enabled ||
    series._hasPointLabels
  ) {
    // Create a separate group for the data labels to avoid rotation
    dataLabelsGroup = series.plotGroup(
      "dataLabelsGroup",
      "data-labels",
      defer && !hasRendered ? "hidden" : "inherit", // #5133, #10220
      seriesDlOptions.zIndex || 6
    );
    if (defer) {
      dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
      if (!hasRendered) {
        setTimeout(function () {
          var group = series.dataLabelsGroup;
          if (group) {
            if (series.visible) {
              // #2597, #3023, #3024
              dataLabelsGroup.show(true);
            }
            group[seriesOptions.animation ? "animate" : "attr"](
              { opacity: 1 },
              { duration: fadeInDuration }
            );
          }
        }, seriesAnimDuration - fadeInDuration);
      }
    }
    // Make the labels for each point
    points.forEach(function (point) {
      // Merge in series options for the point.
      // @note dataLabelAttribs (like pointAttribs) would eradicate
      // the need for dlOptions, and simplify the section below.
      pointOptions = splat(
        mergeArrays(
          seriesDlOptions,
          point.dlOptions || // dlOptions is used in treemaps
            (point.options && point.options.dataLabels)
        )
      );
      // Handle each individual data label for this point
      pointOptions.forEach(function (labelOptions, i) {
        // Options for one datalabel
        var labelEnabled =
            labelOptions.enabled &&
            // #2282, #4641, #7112, #10049
            (!point.isNull || point.dataLabelOnNull) &&
            applyFilter(point, labelOptions),
          labelConfig,
          formatString,
          labelText,
          style,
          rotation,
          attr,
          dataLabel = point.dataLabels ? point.dataLabels[i] : point.dataLabel,
          connector = point.connectors ? point.connectors[i] : point.connector,
          labelDistance = pick(labelOptions.distance, point.labelDistance),
          isNew = !dataLabel;
        if (labelEnabled) {
          // Create individual options structure that can be extended
          // without affecting others
          labelConfig = point.getLabelConfig();
          formatString = pick(
            labelOptions[point.formatPrefix + "Format"],
            labelOptions.format
          );
          labelText = defined(formatString)
            ? format(formatString, labelConfig, chart)
            : (
                labelOptions[point.formatPrefix + "Formatter"] ||
                labelOptions.formatter
              ).call(labelConfig, labelOptions);
          style = labelOptions.style;
          rotation = labelOptions.rotation;
          if (!chart.styledMode) {
            // Determine the color
            style.color = pick(
              labelOptions.color,
              style.color,
              series.color,
              "#000000"
            );
            // Get automated contrast color
            if (style.color === "contrast") {
              point.contrastColor = renderer.getContrast(
                point.color || series.color
              );
              style.color =
                (!defined(labelDistance) && labelOptions.inside) ||
                labelDistance < 0 ||
                !!seriesOptions.stacking
                  ? point.contrastColor
                  : "#000000";
            } else {
              delete point.contrastColor;
            }
            if (seriesOptions.cursor) {
              style.cursor = seriesOptions.cursor;
            }
          }
          attr = {
            r: labelOptions.borderRadius || 0,
            rotation: rotation,
            padding: labelOptions.padding,
            zIndex: 1,
          };
          if (!chart.styledMode) {
            attr.fill = labelOptions.backgroundColor;
            attr.stroke = labelOptions.borderColor;
            attr["stroke-width"] = labelOptions.borderWidth;
          }
          // Remove unused attributes (#947)
          objectEach(attr, function (val, name) {
            if (typeof val === "undefined") {
              delete attr[name];
            }
          });
        }
        // If the point is outside the plot area, destroy it. #678, #820
        if (dataLabel && (!labelEnabled || !defined(labelText))) {
          point.dataLabel = point.dataLabel && point.dataLabel.destroy();
          if (point.dataLabels) {
            // Remove point.dataLabels if this was the last one
            if (point.dataLabels.length === 1) {
              delete point.dataLabels;
            } else {
              delete point.dataLabels[i];
            }
          }
          if (!i) {
            delete point.dataLabel;
          }
          if (connector) {
            point.connector = point.connector.destroy();
            if (point.connectors) {
              // Remove point.connectors if this was the last one
              if (point.connectors.length === 1) {
                delete point.connectors;
              } else {
                delete point.connectors[i];
              }
            }
          }
          // Individual labels are disabled if the are explicitly disabled
          // in the point options, or if they fall outside the plot area.
        } else if (labelEnabled && defined(labelText)) {
          if (!dataLabel) {
            // Create new label element
            point.dataLabels = point.dataLabels || [];
            dataLabel = point.dataLabels[i] = rotation
              ? // Labels don't rotate, use text element
                renderer
                  .text(labelText, 0, -9999)
                  .addClass("highcharts-data-label")
              : // We can use label
                renderer.label(
                  labelText,
                  0,
                  -9999,
                  labelOptions.shape,
                  null,
                  null,
                  labelOptions.useHTML,
                  null,
                  "data-label"
                );
            // Store for backwards compatibility
            if (!i) {
              point.dataLabel = dataLabel;
            }
            dataLabel.addClass(
              " highcharts-data-label-color-" +
                point.colorIndex +
                " " +
                (labelOptions.className || "") + // #3398
                (labelOptions.useHTML ? " highcharts-tracker" : "")
            );
          } else {
            // Use old element and just update text
            attr.text = labelText;
          }
          // Store data label options for later access
          dataLabel.options = labelOptions;
          dataLabel.attr(attr);
          if (!chart.styledMode) {
            // Styles must be applied before add in order to read
            // text bounding box
            dataLabel.css(style).shadow(labelOptions.shadow);
          }
          if (!dataLabel.added) {
            dataLabel.add(dataLabelsGroup);
          }
          if (labelOptions.textPath && !labelOptions.useHTML) {
            dataLabel.setTextPath(
              (point.getDataLabelPath && point.getDataLabelPath(dataLabel)) ||
                point.graphic,
              labelOptions.textPath
            );
            if (point.dataLabelPath && !labelOptions.textPath.enabled) {
              // clean the DOM
              point.dataLabelPath = point.dataLabelPath.destroy();
            }
          }
          // Now the data label is created and placed at 0,0, so we
          // need to align it
          series.alignDataLabel(point, dataLabel, labelOptions, null, isNew);
        }
      });
    });
  }
  H.fireEvent(this, "afterDrawDataLabels");
};
/**
 * Align each individual data label.
 *
 * @private
 * @function Highcharts.Series#alignDataLabel
 * @param {Highcharts.Point} point
 * @param {Highcharts.SVGElement} dataLabel
 * @param {Highcharts.DataLabelsOptionsObject} options
 * @param {Highcharts.BBoxObject} alignTo
 * @param {boolean} [isNew]
 * @return {void}
 */
Series.prototype.alignDataLabel = function (
  point,
  dataLabel,
  options,
  alignTo,
  isNew
) {
  var series = this,
    chart = this.chart,
    inverted = this.isCartesian && chart.inverted,
    enabledDataSorting = this.enabledDataSorting,
    plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999),
    plotY = pick(point.plotY, -9999),
    bBox = dataLabel.getBBox(),
    baseline,
    rotation = options.rotation,
    normRotation,
    negRotation,
    align = options.align,
    rotCorr, // rotation correction
    isInsidePlot = chart.isInsidePlot(plotX, Math.round(plotY), inverted),
    // Math.round for rounding errors (#2683), alignTo to allow column
    // labels (#2700)
    alignAttr, // the final position;
    justify =
      pick(options.overflow, enabledDataSorting ? "none" : "justify") ===
      "justify",
    visible =
      this.visible &&
      (point.series.forceDL ||
        (enabledDataSorting && !justify) ||
        isInsidePlot ||
        (alignTo &&
          chart.isInsidePlot(
            plotX,
            inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1,
            inverted
          ))),
    setStartPos = function (alignOptions) {
      if (enabledDataSorting && series.xAxis && !justify) {
        series.setDataLabelStartPos(
          point,
          dataLabel,
          isNew,
          isInsidePlot,
          alignOptions
        );
      }
    };
  if (visible) {
    baseline = chart.renderer.fontMetrics(
      chart.styledMode ? void 0 : options.style.fontSize,
      dataLabel
    ).b;
    // The alignment box is a singular point
    alignTo = extend(
      {
        x: inverted ? this.yAxis.len - plotY : plotX,
        y: Math.round(inverted ? this.xAxis.len - plotX : plotY),
        width: 0,
        height: 0,
      },
      alignTo
    );
    // Add the text size for alignment calculation
    extend(options, {
      width: bBox.width,
      height: bBox.height,
    });
    // Allow a hook for changing alignment in the last moment, then do the
    // alignment
    if (rotation) {
      justify = false; // Not supported for rotated text
      rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
      alignAttr = {
        x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
        y:
          alignTo.y +
          options.y +
          { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] *
            alignTo.height,
      };
      setStartPos(alignAttr); // data sorting
      dataLabel[isNew ? "attr" : "animate"](alignAttr).attr({
        align: align,
      });
      // Compensate for the rotated label sticking out on the sides
      normRotation = (rotation + 720) % 360;
      negRotation = normRotation > 180 && normRotation < 360;
      if (align === "left") {
        alignAttr.y -= negRotation ? bBox.height : 0;
      } else if (align === "center") {
        alignAttr.x -= bBox.width / 2;
        alignAttr.y -= bBox.height / 2;
      } else if (align === "right") {
        alignAttr.x -= bBox.width;
        alignAttr.y -= negRotation ? 0 : bBox.height;
      }
      dataLabel.placed = true;
      dataLabel.alignAttr = alignAttr;
    } else {
      setStartPos(alignTo); // data sorting
      dataLabel.align(options, null, alignTo);
      alignAttr = dataLabel.alignAttr;
    }
    // Handle justify or crop
    if (justify && alignTo.height >= 0) {
      // #8830
      this.justifyDataLabel(
        dataLabel,
        options,
        alignAttr,
        bBox,
        alignTo,
        isNew
      );
      // Now check that the data label is within the plot area
    } else if (pick(options.crop, true)) {
      visible =
        chart.isInsidePlot(alignAttr.x, alignAttr.y) &&
        chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
    }
    // When we're using a shape, make it possible with a connector or an
    // arrow pointing to thie point
    if (options.shape && !rotation) {
      dataLabel[isNew ? "attr" : "animate"]({
        anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX,
        anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY,
      });
    }
  }
  // To use alignAttr property in hideOverlappingLabels
  if (isNew && enabledDataSorting) {
    dataLabel.placed = false;
  }
  // Show or hide based on the final aligned position
  if (!visible && (!enabledDataSorting || justify)) {
    dataLabel.hide(true);
    dataLabel.placed = false; // don't animate back in
  }
};
/**
 * Set starting position for data label sorting animation.
 *
 * @private
 * @function Highcharts.Series#setDataLabelStartPos
 * @param {Highcharts.SVGElement} dataLabel
 * @param {Highcharts.ColumnPoint} point
 * @param {boolean | undefined} [isNew]
 * @param {boolean} [isInside]
 * @param {Highcharts.AlignObject} [alignOptions]
 *
 * @return {void}
 */
Series.prototype.setDataLabelStartPos = function (
  point,
  dataLabel,
  isNew,
  isInside,
  alignOptions
) {
  var chart = this.chart,
    inverted = chart.inverted,
    xAxis = this.xAxis,
    reversed = xAxis.reversed,
    labelCenter = inverted ? dataLabel.height / 2 : dataLabel.width / 2,
    pointWidth = point.pointWidth,
    halfWidth = pointWidth ? pointWidth / 2 : 0,
    startXPos,
    startYPos;
  startXPos = inverted
    ? alignOptions.x
    : reversed
    ? -labelCenter - halfWidth
    : xAxis.width - labelCenter + halfWidth;
  startYPos = inverted
    ? reversed
      ? this.yAxis.height - labelCenter + halfWidth
      : -labelCenter - halfWidth
    : alignOptions.y;
  dataLabel.startXPos = startXPos;
  dataLabel.startYPos = startYPos;
  // We need to handle visibility in case of sorting point outside plot area
  if (!isInside) {
    dataLabel
      .attr({ opacity: 1 })
      .animate({ opacity: 0 }, void 0, dataLabel.hide);
  } else if (dataLabel.visibility === "hidden") {
    dataLabel.show();
    dataLabel.attr({ opacity: 0 }).animate({ opacity: 1 });
  }
  // Save start position on first render, but do not change position
  if (!chart.hasRendered) {
    return;
  }
  // Set start position
  if (isNew) {
    dataLabel.attr({ x: dataLabel.startXPos, y: dataLabel.startYPos });
  }
  dataLabel.placed = true;
};
/**
 * If data labels fall partly outside the plot area, align them back in, in a
 * way that doesn't hide the point.
 *
 * @private
 * @function Highcharts.Series#justifyDataLabel
 * @param {Highcharts.SVGElement} dataLabel
 * @param {Highcharts.DataLabelsOptionsObject} options
 * @param {Highcharts.SVGAttributes} alignAttr
 * @param {Highcharts.BBoxObject} bBox
 * @param {Highcharts.BBoxObject} [alignTo]
 * @param {boolean} [isNew]
 * @return {boolean|undefined}
 */
Series.prototype.justifyDataLabel = function (
  dataLabel,
  options,
  alignAttr,
  bBox,
  alignTo,
  isNew
) {
  var chart = this.chart,
    align = options.align,
    verticalAlign = options.verticalAlign,
    off,
    justified,
    padding = dataLabel.box ? 0 : dataLabel.padding || 0;
  // Off left
  off = alignAttr.x + padding;
  if (off < 0) {
    if (align === "right") {
      options.align = "left";
      options.inside = true;
    } else {
      options.x = -off;
    }
    justified = true;
  }
  // Off right
  off = alignAttr.x + bBox.width - padding;
  if (off > chart.plotWidth) {
    if (align === "left") {
      options.align = "right";
      options.inside = true;
    } else {
      options.x = chart.plotWidth - off;
    }
    justified = true;
  }
  // Off top
  off = alignAttr.y + padding;
  if (off < 0) {
    if (verticalAlign === "bottom") {
      options.verticalAlign = "top";
      options.inside = true;
    } else {
      options.y = -off;
    }
    justified = true;
  }
  // Off bottom
  off = alignAttr.y + bBox.height - padding;
  if (off > chart.plotHeight) {
    if (verticalAlign === "top") {
      options.verticalAlign = "bottom";
      options.inside = true;
    } else {
      options.y = chart.plotHeight - off;
    }
    justified = true;
  }
  if (justified) {
    dataLabel.placed = !isNew;
    dataLabel.align(options, null, alignTo);
  }
  return justified;
};
if (seriesTypes.pie) {
  seriesTypes.pie.prototype.dataLabelPositioners = {
    // Based on the value computed in Highcharts' distribute algorithm.
    radialDistributionY: function (point) {
      return point.top + point.distributeBox.pos;
    },
    // get the x - use the natural x position for labels near the
    // top and bottom, to prevent the top and botton slice
    // connectors from touching each other on either side
    // Based on the value computed in Highcharts' distribute algorithm.
    radialDistributionX: function (series, point, y, naturalY) {
      return series.getX(
        y < point.top + 2 || y > point.bottom - 2 ? naturalY : y,
        point.half,
        point
      );
    },
    // dataLabels.distance determines the x position of the label
    justify: function (point, radius, seriesCenter) {
      return (
        seriesCenter[0] + (point.half ? -1 : 1) * (radius + point.labelDistance)
      );
    },
    // Left edges of the left-half labels touch the left edge of the plot
    // area. Right edges of the right-half labels touch the right edge of
    // the plot area.
    alignToPlotEdges: function (dataLabel, half, plotWidth, plotLeft) {
      var dataLabelWidth = dataLabel.getBBox().width;
      return half
        ? dataLabelWidth + plotLeft
        : plotWidth - dataLabelWidth - plotLeft;
    },
    // Connectors of each side end in the same x position. Labels are
    // aligned to them. Left edge of the widest left-half label touches the
    // left edge of the plot area. Right edge of the widest right-half label
    // touches the right edge of the plot area.
    alignToConnectors: function (points, half, plotWidth, plotLeft) {
      var maxDataLabelWidth = 0,
        dataLabelWidth;
      // find widest data label
      points.forEach(function (point) {
        dataLabelWidth = point.dataLabel.getBBox().width;
        if (dataLabelWidth > maxDataLabelWidth) {
          maxDataLabelWidth = dataLabelWidth;
        }
      });
      return half
        ? maxDataLabelWidth + plotLeft
        : plotWidth - maxDataLabelWidth - plotLeft;
    },
  };
  /**
   * Override the base drawDataLabels method by pie specific functionality
   *
   * @private
   * @function Highcharts.seriesTypes.pie#drawDataLabels
   * @return {void}
   */
  seriesTypes.pie.prototype.drawDataLabels = function () {
    var series = this,
      data = series.data,
      point,
      chart = series.chart,
      options = series.options.dataLabels,
      connectorPadding = options.connectorPadding,
      connectorWidth,
      plotWidth = chart.plotWidth,
      plotHeight = chart.plotHeight,
      plotLeft = chart.plotLeft,
      maxWidth = Math.round(chart.chartWidth / 3),
      connector,
      seriesCenter = series.center,
      radius = seriesCenter[2] / 2,
      centerY = seriesCenter[1],
      dataLabel,
      dataLabelWidth,
      // labelPos,
      labelPosition,
      labelHeight,
      // divide the points into right and left halves for anti collision
      halves = [
        [],
        [], // left
      ],
      x,
      y,
      visibility,
      j,
      overflow = [0, 0, 0, 0], // top, right, bottom, left
      dataLabelPositioners = series.dataLabelPositioners,
      pointDataLabelsOptions;
    // get out if not enabled
    if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
      return;
    }
    // Reset all labels that have been shortened
    data.forEach(function (point) {
      if (point.dataLabel && point.visible && point.dataLabel.shortened) {
        point.dataLabel
          .attr({
            width: "auto",
          })
          .css({
            width: "auto",
            textOverflow: "clip",
          });
        point.dataLabel.shortened = false;
      }
    });
    // run parent method
    Series.prototype.drawDataLabels.apply(series);
    data.forEach(function (point) {
      if (point.dataLabel) {
        if (point.visible) {
          // #407, #2510
          // Arrange points for detection collision
          halves[point.half].push(point);
          // Reset positions (#4905)
          point.dataLabel._pos = null;
          // Avoid long labels squeezing the pie size too far down
          if (
            !defined(options.style.width) &&
            !defined(
              point.options.dataLabels &&
                point.options.dataLabels.style &&
                point.options.dataLabels.style.width
            )
          ) {
            if (point.dataLabel.getBBox().width > maxWidth) {
              point.dataLabel.css({
                // Use a fraction of the maxWidth to avoid
                // wrapping close to the end of the string.
                width: maxWidth * 0.7,
              });
              point.dataLabel.shortened = true;
            }
          }
        } else {
          point.dataLabel = point.dataLabel.destroy();
          // Workaround to make pies destroy multiple datalabels
          // correctly. This logic needs rewriting to support multiple
          // datalabels fully.
          if (point.dataLabels && point.dataLabels.length === 1) {
            delete point.dataLabels;
          }
        }
      }
    });
    /* Loop over the points in each half, starting from the top and bottom
     * of the pie to detect overlapping labels.
     */
    halves.forEach(function (points, i) {
      var top,
        bottom,
        length = points.length,
        positions = [],
        naturalY,
        sideOverflow,
        size,
        distributionLength;
      if (!length) {
        return;
      }
      // Sort by angle
      series.sortByAngle(points, i - 0.5);
      // Only do anti-collision when we have dataLabels outside the pie
      // and have connectors. (#856)
      if (series.maxLabelDistance > 0) {
        top = Math.max(0, centerY - radius - series.maxLabelDistance);
        bottom = Math.min(
          centerY + radius + series.maxLabelDistance,
          chart.plotHeight
        );
        points.forEach(function (point) {
          // check if specific points' label is outside the pie
          if (point.labelDistance > 0 && point.dataLabel) {
            // point.top depends on point.labelDistance value
            // Used for calculation of y value in getX method
            point.top = Math.max(0, centerY - radius - point.labelDistance);
            point.bottom = Math.min(
              centerY + radius + point.labelDistance,
              chart.plotHeight
            );
            size = point.dataLabel.getBBox().height || 21;
            // point.positionsIndex is needed for getting index of
            // parameter related to specific point inside positions
            // array - not every point is in positions array.
            point.distributeBox = {
              target: point.labelPosition.natural.y - point.top + size / 2,
              size: size,
              rank: point.y,
            };
            positions.push(point.distributeBox);
          }
        });
        distributionLength = bottom + size - top;
        H.distribute(positions, distributionLength, distributionLength / 5);
      }
      // Now the used slots are sorted, fill them up sequentially
      for (j = 0; j < length; j++) {
        point = points[j];
        // labelPos = point.labelPos;
        labelPosition = point.labelPosition;
        dataLabel = point.dataLabel;
        visibility = point.visible === false ? "hidden" : "inherit";
        naturalY = labelPosition.natural.y;
        y = naturalY;
        if (positions && defined(point.distributeBox)) {
          if (typeof point.distributeBox.pos === "undefined") {
            visibility = "hidden";
          } else {
            labelHeight = point.distributeBox.size;
            // Find label's y position
            y = dataLabelPositioners.radialDistributionY(point);
          }
        }
        // It is needed to delete point.positionIndex for
        // dynamically added points etc.
        delete point.positionIndex; // @todo unused
        // Find label's x position
        // justify is undocumented in the API - preserve support for it
        if (options.justify) {
          x = dataLabelPositioners.justify(point, radius, seriesCenter);
        } else {
          switch (options.alignTo) {
            case "connectors":
              x = dataLabelPositioners.alignToConnectors(
                points,
                i,
                plotWidth,
                plotLeft
              );
              break;
            case "plotEdges":
              x = dataLabelPositioners.alignToPlotEdges(
                dataLabel,
                i,
                plotWidth,
                plotLeft
              );
              break;
            default:
              x = dataLabelPositioners.radialDistributionX(
                series,
                point,
                y,
                naturalY
              );
          }
        }
        // Record the placement and visibility
        dataLabel._attr = {
          visibility: visibility,
          align: labelPosition.alignment,
        };
        dataLabel._pos = {
          x:
            x +
            options.x +
            ({
              left: connectorPadding,
              right: -connectorPadding,
            }[labelPosition.alignment] || 0),
          // 10 is for the baseline (label vs text)
          y: y + options.y - 10,
        };
        // labelPos.x = x;
        // labelPos.y = y;
        labelPosition.final.x = x;
        labelPosition.final.y = y;
        // Detect overflowing data labels
        if (pick(options.crop, true)) {
          dataLabelWidth = dataLabel.getBBox().width;
          sideOverflow = null;
          // Overflow left
          if (
            x - dataLabelWidth < connectorPadding &&
            i === 1 // left half
          ) {
            sideOverflow = Math.round(dataLabelWidth - x + connectorPadding);
            overflow[3] = Math.max(sideOverflow, overflow[3]);
            // Overflow right
          } else if (
            x + dataLabelWidth > plotWidth - connectorPadding &&
            i === 0 // right half
          ) {
            sideOverflow = Math.round(
              x + dataLabelWidth - plotWidth + connectorPadding
            );
            overflow[1] = Math.max(sideOverflow, overflow[1]);
          }
          // Overflow top
          if (y - labelHeight / 2 < 0) {
            overflow[0] = Math.max(
              Math.round(-y + labelHeight / 2),
              overflow[0]
            );
            // Overflow left
          } else if (y + labelHeight / 2 > plotHeight) {
            overflow[2] = Math.max(
              Math.round(y + labelHeight / 2 - plotHeight),
              overflow[2]
            );
          }
          dataLabel.sideOverflow = sideOverflow;
        }
      } // for each point
    }); // for each half
    // Do not apply the final placement and draw the connectors until we
    // have verified that labels are not spilling over.
    if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
      // Place the labels in the final position
      this.placeDataLabels();
      this.points.forEach(function (point) {
        // #8864: every connector can have individual options
        pointDataLabelsOptions = merge(options, point.options.dataLabels);
        connectorWidth = pick(pointDataLabelsOptions.connectorWidth, 1);
        // Draw the connector
        if (connectorWidth) {
          var isNew;
          connector = point.connector;
          dataLabel = point.dataLabel;
          if (
            dataLabel &&
            dataLabel._pos &&
            point.visible &&
            point.labelDistance > 0
          ) {
            visibility = dataLabel._attr.visibility;
            isNew = !connector;
            if (isNew) {
              point.connector = connector = chart.renderer
                .path()
                .addClass(
                  "highcharts-data-label-connector " +
                    " highcharts-color-" +
                    point.colorIndex +
                    (point.className ? " " + point.className : "")
                )
                .add(series.dataLabelsGroup);
              if (!chart.styledMode) {
                connector.attr({
                  "stroke-width": connectorWidth,
                  stroke:
                    pointDataLabelsOptions.connectorColor ||
                    point.color ||
                    "#666666",
                });
              }
            }
            connector[isNew ? "attr" : "animate"]({
              d: point.getConnectorPath(),
            });
            connector.attr("visibility", visibility);
          } else if (connector) {
            point.connector = connector.destroy();
          }
        }
      });
    }
  };
  /**
   * Extendable method for getting the path of the connector between the data
   * label and the pie slice.
   *
   * @private
   * @function Highcharts.seriesTypes.pie#connectorPath
   *
   * @param {*} labelPos
   *
   * @return {Highcharts.SVGPathArray}
   */
  // TODO: depracated - remove it
  /*
    seriesTypes.pie.prototype.connectorPath = function (labelPos) {
        var x = labelPos.x,
            y = labelPos.y;
        return pick(this.options.dataLabels.softConnector, true) ? [
            'M',
            // end of the string at the label
            x + (labelPos[6] === 'left' ? 5 : -5), y,
            'C',
            x, y, // first break, next to the label
            2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
            labelPos[2], labelPos[3], // second break
            'L',
            labelPos[4], labelPos[5] // base
        ] : [
            'M',
            // end of the string at the label
            x + (labelPos[6] === 'left' ? 5 : -5), y,
            'L',
            labelPos[2], labelPos[3], // second break
            'L',
            labelPos[4], labelPos[5] // base
        ];
    };
    */
  /**
   * Perform the final placement of the data labels after we have verified
   * that they fall within the plot area.
   *
   * @private
   * @function Highcharts.seriesTypes.pie#placeDataLabels
   * @return {void}
   */
  seriesTypes.pie.prototype.placeDataLabels = function () {
    this.points.forEach(function (point) {
      var dataLabel = point.dataLabel,
        _pos;
      if (dataLabel && point.visible) {
        _pos = dataLabel._pos;
        if (_pos) {
          // Shorten data labels with ellipsis if they still overflow
          // after the pie has reached minSize (#223).
          if (dataLabel.sideOverflow) {
            dataLabel._attr.width = Math.max(
              dataLabel.getBBox().width - dataLabel.sideOverflow,
              0
            );
            dataLabel.css({
              width: dataLabel._attr.width + "px",
              textOverflow:
                (this.options.dataLabels.style || {}).textOverflow ||
                "ellipsis",
            });
            dataLabel.shortened = true;
          }
          dataLabel.attr(dataLabel._attr);
          dataLabel[dataLabel.moved ? "animate" : "attr"](_pos);
          dataLabel.moved = true;
        } else if (dataLabel) {
          dataLabel.attr({ y: -9999 });
        }
      }
      // Clear for update
      delete point.distributeBox;
    }, this);
  };
  seriesTypes.pie.prototype.alignDataLabel = noop;
  /**
   * Verify whether the data labels are allowed to draw, or we should run more
   * translation and data label positioning to keep them inside the plot area.
   * Returns true when data labels are ready to draw.
   *
   * @private
   * @function Highcharts.seriesTypes.pie#verifyDataLabelOverflow
   * @param {Array<number>} overflow
   * @return {boolean}
   */
  seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
    var center = this.center,
      options = this.options,
      centerOption = options.center,
      minSize = options.minSize || 80,
      newSize = minSize,
      // If a size is set, return true and don't try to shrink the pie
      // to fit the labels.
      ret = options.size !== null;
    if (!ret) {
      // Handle horizontal size and center
      if (centerOption[0] !== null) {
        // Fixed center
        newSize = Math.max(
          center[2] - Math.max(overflow[1], overflow[3]),
          minSize
        );
      } else {
        // Auto center
        newSize = Math.max(
          // horizontal overflow
          center[2] - overflow[1] - overflow[3],
          minSize
        );
        // horizontal center
        center[0] += (overflow[3] - overflow[1]) / 2;
      }
      // Handle vertical size and center
      if (centerOption[1] !== null) {
        // Fixed center
        newSize = clamp(
          newSize,
          minSize,
          center[2] - Math.max(overflow[0], overflow[2])
        );
      } else {
        // Auto center
        newSize = clamp(
          newSize,
          minSize,
          // vertical overflow
          center[2] - overflow[0] - overflow[2]
        );
        // vertical center
        center[1] += (overflow[0] - overflow[2]) / 2;
      }
      // If the size must be decreased, we need to run translate and
      // drawDataLabels again
      if (newSize < center[2]) {
        center[2] = newSize;
        center[3] = Math.min(
          // #3632
          relativeLength(options.innerSize || 0, newSize),
          newSize
        );
        this.translate(center);
        if (this.drawDataLabels) {
          this.drawDataLabels();
        }
        // Else, return true to indicate that the pie and its labels is
        // within the plot area
      } else {
        ret = true;
      }
    }
    return ret;
  };
}
if (seriesTypes.column) {
  /**
   * Override the basic data label alignment by adjusting for the position of
   * the column.
   *
   * @private
   * @function Highcharts.seriesTypes.column#alignDataLabel
   * @param {Highcharts.Point} point
   * @param {Highcharts.SVGElement} dataLabel
   * @param {Highcharts.DataLabelsOptionsObject} options
   * @param {Highcharts.BBoxObject} alignTo
   * @param {boolean} [isNew]
   * @return {void}
   */
  seriesTypes.column.prototype.alignDataLabel = function (
    point,
    dataLabel,
    options,
    alignTo,
    isNew
  ) {
    var inverted = this.chart.inverted,
      series = point.series,
      // data label box for alignment
      dlBox = point.dlBox || point.shapeArgs,
      below = pick(
        point.below, // range series
        point.plotY > pick(this.translatedThreshold, series.yAxis.len)
      ),
      // draw it inside the box?
      inside = pick(options.inside, !!this.options.stacking),
      overshoot;
    // Align to the column itself, or the top of it
    if (dlBox) {
      // Area range uses this method but not alignTo
      alignTo = merge(dlBox);
      if (alignTo.y < 0) {
        alignTo.height += alignTo.y;
        alignTo.y = 0;
      }
      overshoot = alignTo.y + alignTo.height - series.yAxis.len;
      if (overshoot > 0) {
        alignTo.height -= overshoot;
      }
      if (inverted) {
        alignTo = {
          x: series.yAxis.len - alignTo.y - alignTo.height,
          y: series.xAxis.len - alignTo.x - alignTo.width,
          width: alignTo.height,
          height: alignTo.width,
        };
      }
      // Compute the alignment box
      if (!inside) {
        if (inverted) {
          alignTo.x += below ? 0 : alignTo.width;
          alignTo.width = 0;
        } else {
          alignTo.y += below ? alignTo.height : 0;
          alignTo.height = 0;
        }
      }
    }
    // When alignment is undefined (typically columns and bars), display the
    // individual point below or above the point depending on the threshold
    options.align = pick(
      options.align,
      !inverted || inside ? "center" : below ? "right" : "left"
    );
    options.verticalAlign = pick(
      options.verticalAlign,
      inverted || inside ? "middle" : below ? "top" : "bottom"
    );
    // Call the parent method
    Series.prototype.alignDataLabel.call(
      this,
      point,
      dataLabel,
      options,
      alignTo,
      isNew
    );
    // Hide dataLabel when column is outside plotArea (#12370).
    if (
      alignTo &&
      ((alignTo.height <= 0 && alignTo.y === this.chart.plotHeight) ||
        (alignTo.width <= 0 && alignTo.x === 0))
    ) {
      dataLabel.hide(true);
      dataLabel.placed = false; // don't animate back in
    }
    // If label was justified and we have contrast, set it:
    if (options.inside && point.contrastColor) {
      dataLabel.css({
        color: point.contrastColor,
      });
    }
  };
}
